﻿using System;
using System.IO;
using System.Xml.Serialization;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace TestListGenerator
{
    public class VsmdiHelper
    {
        #region Static Mappings
        /// <summary>
        /// We need to statically map these keys for now, since I don't
        /// know how to generate them :(
        /// </summary>
        public enum VsmdiStaticMapKey
        {
            /// <summary>
            /// The default test list, needed by Visual Studio
            /// </summary>
            DefaultTestList,
            /// <summary>
            /// Included Tests Only
            /// </summary>
            IncludedTestsTestList,
            /// <summary>
            /// Excluded Tests Only
            /// </summary>
            ExcludedTestsTestList,
            /// <summary>
            /// Teh default test run configuration
            /// </summary>
            DefaultRunConfiguration
        }


        /// <summary>
        /// Used to statically map names to IDs for now
        /// </summary>
        public class VsmdiStaticMapEntry
        {
            public string Name;
            public string ID;
        }

        /// <summary>
        /// Since I can't work out how to generate project list IDs
        /// or RunConfiguration IDs, lets hack them for now
        /// </summary>
        public static Dictionary<VsmdiStaticMapKey, VsmdiStaticMapEntry> VsmdiStaticMap =
            new Dictionary<VsmdiStaticMapKey, VsmdiStaticMapEntry>
        {
            {VsmdiStaticMapKey.IncludedTestsTestList, new VsmdiStaticMapEntry { Name="Included", ID="f4d29cb1-c6bb-4787-aa0f-3f91ec11a170"}},
            {VsmdiStaticMapKey.ExcludedTestsTestList, new VsmdiStaticMapEntry { Name="Excluded", ID="a9d90125-bd17-4fc9-b97f-38aa981829aa"}},
            {VsmdiStaticMapKey.DefaultTestList, new VsmdiStaticMapEntry { Name="Lists of Tests", ID="8c43106b-9dc1-4907-a29f-aa66a61bf5b6"}},
            {VsmdiStaticMapKey.DefaultRunConfiguration, new VsmdiStaticMapEntry { Name="Local Test Run", ID="5b27d636-1ffb-4592-8872-eb34e2d09d24"}}
        };
        #endregion

        /// <summary>
        /// VSMDI has it's own proprietary ID format. This function 
        /// takes a string and converts it to the appropriate GUID ID
        /// More information on this process can be found here:
        /// http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=157039&SiteID=1
        /// </summary>
        /// <param name="toConvert">The string to convert into an ID</param>
        /// <returns>The string, with a SHA1 transformation into a GUID</returns>
        public static string GenerateVsmdiID(string toConvert) 
        {
            // Null check
            if (toConvert == null)
            {
                throw new ArgumentNullException("toConvert", "Unable to convert a null stirng to a VSMDI ID");
            }
            Trace.WriteLine("Creating VSMDI ID for " + toConvert);

            // Hash the string to create a 16 byte GUID
            SHA1Managed crypto = new SHA1Managed();
            byte[] bytes = new byte[16];
            Array.Copy(crypto.ComputeHash(Encoding.Unicode.GetBytes(toConvert)), bytes, 16);
            Guid guid = new Guid(bytes); 
            return guid.ToString();
        }

        /// <summary>
        /// To create the VSMDI ID for a method signature, you just need
        /// to take the full name of the method and hash it using the
        /// GenerateVsmdiID method
        /// </summary>
        /// <param name="mi">The method to retrieve the ID for</param>
        /// <returns>A GUID representing the signature of the method</returns>
        public static string GenerateVsmdiID(MethodInfo mi)
        {
            // Null check
            if (mi == null)
            {
                throw new ArgumentNullException("mi", "You need to pass in a method in order to generate the ID");
            }
            return GenerateVsmdiID(mi.DeclaringType.FullName + '.' + mi.Name);
        }



        /// <summary>
        /// Creates the default test list which is needed for the other test lists
        /// to be created
        /// </summary>
        /// <returns>A TestListsTestList object for the default test</returns>
        public static TestListsTestList CreateDefaultTestList()
        {
            // Get the static mapping for the default test list
            VsmdiStaticMapEntry testListStaticMapping = VsmdiStaticMap[VsmdiStaticMapKey.DefaultTestList];

            // get the static mapping for the default test run config
            VsmdiStaticMapEntry runConfigStaticMapping = VsmdiStaticMap[VsmdiStaticMapKey.DefaultRunConfiguration];

            // Should end up looking like this:
            //  <TestList name="Lists of Tests" id="8c43106b-9dc1-4907-a29f-aa66a61bf5b6">
            //    <RunConfiguration id="d0fd86fa-f02f-47a6-ac94-1e966ef3564e" name="Local Test Run" storage="localtestrun.testrunconfig" type="Microsoft.VisualStudio.TestTools.Common.TestRunConfiguration, Microsoft.VisualStudio.QualityTools.Common,   PublicKeyToken=b03f5f7f11d50a3a" />
            // </TestList>
            TestListsTestList defaultTestList = new TestListsTestList
            {
                name = testListStaticMapping.Name,
                id = testListStaticMapping.ID,
                RunConfiguration = new TestListsTestListRunConfiguration
                {
                    name = runConfigStaticMapping.Name,
                    id = runConfigStaticMapping.ID,
                    storage = "localtestrun.testrunconfig",
                    type = RUN_CONFIG_TYPE
                }
            };

            return defaultTestList;
        }

        /// <summary>
        /// Creates a list test list from a list of tests
        /// </summary>
        /// <param name="dllPath">The path to the DLL that we will run the Tests from</param>
        /// <param name="listType">The type of list we are creating: Included or Excluded. This is used
        /// to lookup the appropriate static map entry to work out the ID / Name of the TestList</param>
        /// <param name="parentID">The ListID of the parent list for this list. If set to null, it will use Microsoft's default test list parent</param>
        /// <param name="tests">A mapping of MethodInfo's to the tests you want to include in the test list</param>
        /// <returns>A TestList object that includes a list of the Tests passed in</returns>
        public static TestListsTestList CreateTestList(string dllPath, VsmdiStaticMapKey listType, string parentID, ICollection<MethodInfo> tests)
        {
            // Look up the static mapping
            VsmdiStaticMapEntry testListStaticMapping = VsmdiStaticMap[listType];

            // Pass the request on to the funciton that does the work
            return CreateTestList(dllPath, testListStaticMapping.Name, testListStaticMapping.ID, parentID, tests);
        }

        /// <summary>
        /// Creates a list test list from a list of tests
        /// </summary>
        /// <param name="dllPath">The path to the DLL that we will run the Tests from</param>
        /// <param name="id">The ListID of the TestList you want to create</param>
        /// <param name="name">The Name of the TestList you want to create</param>
        /// <param name="parentID">The ListID of the parent list for this list. If set to null, it will use Microsoft's default test list parent</param>
        /// <param name="tests">A mapping of MethodInfo's to the tests you want to include in the test list</param>
        /// <returns>A TestList object that includes a list of the Tests passed in</returns>
        public static TestListsTestList CreateTestList(string dllPath, string name, string id, string parentID, ICollection<MethodInfo> tests)
        {
            // Check for nulls
            if (dllPath == null)
            {
                throw new ArgumentNullException("dllPath", "Unable to create a test list as you didn't provide a path to the DLL we are testing");
            }
            if (name == null)
            {
                throw new ArgumentNullException("name", "Unable to create a test list as the name passed in was null");
            }
            if (id == null)
            {
                throw new ArgumentNullException("id", "Unable to create a test list as the id passed in was null");
            }
            // If the parentID is null, then use the Default List ID
            if (parentID == null)
            {
                VsmdiStaticMapEntry defaultTestListStaticMapping = VsmdiStaticMap[VsmdiStaticMapKey.DefaultTestList];
                parentID = defaultTestListStaticMapping.ID;
            }

            // Create the test list
            TestListsTestList result = new TestListsTestList
            {
                name = name,
                id = id,
                parentListId = parentID
            };

            // Check to see if there are tests to add
            if (tests != null) 
            {
                result.TestLinks = CreateTestLinks(dllPath, tests);
            }

            return result;

        }

        /// <summary>
        /// This method creates the list of TestLinks for a collection of Tests. 
        /// </summary>
        /// <param name="dllPath">The DLL the tests reside in</param>
        /// <param name="tests">A list of Tests to export to TestLinks</param>
        /// <returns>A TestLink array that represents a list of Tests to run in a TestList</returns>
        public static TestListsTestListTestLink[] CreateTestLinks(string dllPath, ICollection<MethodInfo> tests)
        {
            // Null checks
            if (dllPath == null)
            {
                throw new ArgumentNullException("dllPath", "The DLL Path needs to be non null in order to set the storage path for the TestLinks");
            }
            if (tests == null)
            {
                throw new ArgumentNullException("tests", "You need to provide a list of tests you want to add to the TestLink collection");
            }

            // Cycle through the tests colleciton, making a test link for each one
            List<TestListsTestListTestLink> testLinks = new List<TestListsTestListTestLink>(tests.Count);
            foreach (MethodInfo mi in tests)
            {
                testLinks.Add(
                    new TestListsTestListTestLink
                    {
                        name = mi.Name,
                        id = GenerateVsmdiID(mi),
                        storage = dllPath,
                        type = UNIT_TEST_ELEMENT_TYPE
                    }
                );
            }

            // Covnert the list to an array
            return testLinks.ToArray();
        }

        /// <summary>
        /// Once you have populated the TestList objects you want to write out to the VSMDI file, this
        /// method will add in the default TestList and then write the file out to the outputPath 
        /// specified
        /// </summary>
        /// <param name="outputPath">The path to the DLL you wish to write the TestLists for</param>
        /// <param name="testLists">The lists you want to write out the outputPath file</param>
        public static void CreateVsmdiConfigFile(string outputPath, TestListsTestList[] testLists) 
        {
            if (outputPath == null)
            {
                throw new ArgumentNullException("outputPath", "You need to pass in an output path to write the file out to");
            }
            if (testLists == null)
            {
                throw new ArgumentNullException("testLists", "You need to provide a list of tests to write out. To write out no tests, create an array with 0 elements");
            }
            // Get the default test list 
            TestListsTestList[] defaultTestList = new TestListsTestList[]{};

            // Add in the default test list
            TestListsTestList[] combinedTestList = new TestListsTestList[testLists.Length + 1];
            combinedTestList[0] = CreateDefaultTestList();
            testLists.CopyTo(combinedTestList, 1);

            // Create the TestList container
            TestLists testListContainer = new TestLists
            {
                 TestList = combinedTestList
            };

            // Write it out to the path given in
            XmlSerializer ser = new XmlSerializer(typeof(TestLists));
            using (StreamWriter sw = new StreamWriter(outputPath)) ser.Serialize(sw, testListContainer);
        }

        /// <summary>
        /// Reads in a VSMDI config file, and converts it to a test list
        /// </summary>
        /// <param name="path">The path to read in</param>
        /// <returns>A TestLists object populated from the config file</returns>
        public static TestLists ReadVsmdiConfigFile(string path) 
        {
            if (path == null)
            {
                throw new ArgumentNullException("path", "You need to provide a path to read the config file in from");
            }
            XmlSerializer ser = new XmlSerializer(typeof(TestLists));
            using (StreamReader sr = new StreamReader(path)) return (TestLists)ser.Deserialize(sr);
        }

        public const string UNIT_TEST_ELEMENT_TYPE = "Microsoft.VisualStudio.TestTools.TestTypes.Unit.UnitTestElement, Microsoft.VisualStudio.QualityTools.Tips.UnitTest.ObjectModel,   PublicKeyToken=b03f5f7f11d50a3a";
        public const string RUN_CONFIG_TYPE = "Microsoft.VisualStudio.TestTools.Common.TestRunConfiguration, Microsoft.VisualStudio.QualityTools.Common,   PublicKeyToken=b03f5f7f11d50a3a";
    }


}
